package com.xckj.cycling.view

import android.content.Context
import android.graphics.Color
import android.location.Location
import android.util.AttributeSet
import androidx.lifecycle.LifecycleOwner
import com.xckj.cycling.framework.lazyGlobalViewModel
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.plugin.animation.MapAnimationOptions
import com.mapbox.maps.plugin.animation.camera
import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
import com.mapbox.navigation.base.formatter.UnitType
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.trip.model.RouteProgress
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.replay.MapboxReplayer
import com.mapbox.navigation.core.replay.ReplayLocationEngine
import com.mapbox.navigation.core.replay.route.ReplayProgressObserver
import com.mapbox.navigation.core.replay.route.ReplayRouteMapper
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.ui.maneuver.api.MapboxManeuverApi
import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
import com.mapbox.navigation.ui.maps.route.RouteLayerConstants
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
import com.mapbox.navigation.ui.maps.route.line.model.MapboxRouteLineOptions
import com.mapbox.navigation.ui.maps.route.line.model.RouteLineColorResources
import com.mapbox.navigation.ui.maps.route.line.model.RouteLineResources
import com.xckj.cycling.R
import com.xckj.cycling.context.AppContext
import com.xckj.cycling.viewmodel.MapViewModel
import java.util.*

open class RouteMapView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : CMapView(context, attrs) {
    companion object {
        @JvmStatic
        val viewModel: MapViewModel by lazyGlobalViewModel()

        @JvmStatic
        val mapboxNavigation: MapboxNavigation =
            (MapboxNavigationApp.current() ?: MapboxNavigation(
                NavigationOptions.Builder(AppContext.globalContext())
                    .accessToken(AppContext.globalContext().getString(R.string.mapbox_access_token))
                    .build()
            ))
    }

    /**
     * [NavigationLocationProvider] is a utility class that helps to provide location updates generated by the Navigation SDK
     * to the Maps SDK in order to update the user location indicator on the map.
     */
    val navigationLocationProvider by lazy {
        NavigationLocationProvider()
    }

    /**
     * RouteLine: Various route line related options can be customized here including applying
     * route line color customizations.
     */
    private val routeLineResources: RouteLineResources by lazy {
        RouteLineResources.Builder()
            /**
             * Route line related colors can be customized via the [RouteLineColorResources]. If using the
             * default colors the [RouteLineColorResources] does not need to be set as seen here, the
             * defaults will be used internally by the builder.
             */
            .routeLineColorResources(
                RouteLineColorResources.Builder().routeDefaultColor(Color.RED)
                    .alternativeRouteDefaultColor(Color.RED).build()
            )
            .build()
    }

    /**
     * RouteLine: Additional route line options are available through the MapboxRouteLineOptions.
     * Notice here the withRouteLineBelowLayerId option. The map is made up of layers. In this
     * case the route line will be placed below the "road-label" layer which is a good default
     * for the most common Mapbox navigation related maps. You should consider if this should be
     * changed for your use case especially if you are using a custom map style.
     */
    private val options: MapboxRouteLineOptions by lazy {
        MapboxRouteLineOptions.Builder(requireContext())
            /**
             * Remove this line and [onPositionChangedListener] if you don't wish to show the
             * vanishing route line feature
             */
            .withVanishingRouteLineEnabled(true)
            .withRouteLineResources(routeLineResources)
            .withRouteLineBelowLayerId("road-label")
            .build()
    }

    /**
     * RouteLine: This class is responsible for rendering route line related mutations generated
     * by the [routeLineApi]
     */
    protected val routeLineView by lazy {
        MapboxRouteLineView(options)
    }

    /**
     * RouteLine: This class is responsible for generating route line related data which must be
     * rendered by the [routeLineView] in order to visualize the route line on the map.
     */
    protected val routeLineApi: MapboxRouteLineApi by lazy {
        MapboxRouteLineApi(options)
    }

    /**
     * RouteArrow: This class is responsible for generating data related to maneuver arrows. The
     * data generated must be rendered by the [routeArrowView] in order to apply mutations to
     * the map.
     */
    private val routeArrowApi: MapboxRouteArrowApi by lazy {
        MapboxRouteArrowApi()
    }

    /**
     * RouteArrow: Customization of the maneuver arrow(s) can be done using the
     * [RouteArrowOptions]. Here the above layer ID is used to determine where in the map layer
     * stack the arrows appear. Above the layer of the route traffic line is being used here. Your
     * use case may necessitate adjusting this to a different layer position.
     */
    private val routeArrowOptions by lazy {
        RouteArrowOptions.Builder(requireContext())
            .withAboveLayerId(RouteLayerConstants.TOP_LEVEL_ROUTE_LINE_LAYER_ID)
            .build()
    }

    /**
     * RouteArrow: This class is responsible for rendering the arrow related mutations generated
     * by the [routeArrowApi]
     */
    private val routeArrowView: MapboxRouteArrowView by lazy {
        MapboxRouteArrowView(routeArrowOptions)
    }

    /**
     * RouteLine: This is one way to keep the route(s) appearing on the map in sync with
     * MapboxNavigation. When this observer is called the route data is used to draw route(s)
     * on the map.
     */
    private val routesObserver: RoutesObserver = RoutesObserver { routeUpdateResult ->
// RouteLine: wrap the NavigationRoute objects and pass them
// to the MapboxRouteLineApi to generate the data necessary to draw the route(s)
// on the map.
        routeLineApi.setNavigationRoutes(
            routeUpdateResult.navigationRoutes
        ) { value ->
// RouteLine: The MapboxRouteLineView expects a non-null reference to the map style.
// the data generated by the call to the MapboxRouteLineApi above must be rendered
// by the MapboxRouteLineView in order to visualize the changes on the map.
            getMapboxMap().getStyle()?.apply {
                routeLineView.renderRouteDrawData(this, value)
            }
        }
    }

    /**
     * RouteLine: This listener is necessary only when enabling the vanishing route line feature
     * which changes the color of the route line behind the puck during navigation. If this
     * option is set to `false` (the default) in MapboxRouteLineOptions then it is not necessary
     * to use this listener.
     */
    private val onPositionChangedListener = OnIndicatorPositionChangedListener { point ->
        val result = routeLineApi.updateTraveledRouteLine(point)
        getMapboxMap().getStyle()?.apply {
// Render the result to update the map.
            routeLineView.renderRouteLineUpdate(this, result)
        }
    }

    private val routeProgressObserver = RouteProgressObserver { routeProgress ->
// RouteLine: This line is only necessary if the vanishing route line feature
// is enabled.
        maneuvers(routeProgress)
        routeLineApi.updateWithRouteProgress(routeProgress) { result ->
            getMapboxMap().getStyle()?.apply {
                routeLineView.renderRouteLineUpdate(this, result)
            }
        }

// RouteArrow: The next maneuver arrows are driven by route progress events.
// Generate the next maneuver arrow update data and pass it to the view class
// to visualize the updates on the map.
        val arrowUpdate = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
        getMapboxMap().getStyle()?.apply {
// Render the result to update the map.
            routeArrowView.renderManeuverUpdate(this, arrowUpdate)
        }
    }

    // Define distance formatter options
    protected val distanceFormatter: MapboxDistanceFormatter by lazy {
        MapboxDistanceFormatter(
            DistanceFormatterOptions.Builder(requireContext()).unitType(UnitType.IMPERIAL).build()
        )
    }

    // Create an instance of the Maneuver API
    protected val maneuverApi: MapboxManeuverApi by lazy {
        MapboxManeuverApi(distanceFormatter)
    }

    open fun maneuvers(routeProgress: RouteProgress) {

    }

    private val locationObserver = object : LocationObserver {
        override fun onNewRawLocation(rawLocation: Location) {}
        override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
            val enhancedLocation = locationMatcherResult.enhancedLocation
            viewModel.locationChanged(enhancedLocation)
            viewModel.lastRecordLocation = enhancedLocation
            navigationLocationProvider.changePosition(
                enhancedLocation,
                locationMatcherResult.keyPoints,
            )
            if (this@RouteMapView is NavigationMapView || this@RouteMapView is NavigationToStartMapView) {
                updateCamera(
                    Point.fromLngLat(
                        enhancedLocation.longitude, enhancedLocation.latitude
                    ),
                    enhancedLocation.bearing.toDouble()
                )
            }
        }
    }


    override fun onCreate(owner: LifecycleOwner) {
        super.onCreate(owner)
        initNavigation()
        mapboxNavigation.registerRoutesObserver(routesObserver)
        mapboxNavigation.registerLocationObserver(locationObserver)
        mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
        mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)
    }

    override fun onDestroy(owner: LifecycleOwner) {
        super.onDestroy(owner)
        mapboxNavigation.unregisterRoutesObserver(routesObserver)
        mapboxNavigation.unregisterLocationObserver(locationObserver)
        mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
        mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver)
    }


    /**
     * Debug tool used to play, pause and seek route progress events that can be used to produce mocked location updates along the route.
     */
    private val mapboxReplayer = MapboxReplayer()

    /**
     * Debug observer that makes sure the replayer has always an up-to-date information to generate mock updates.
     */
    private val replayProgressObserver = ReplayProgressObserver(mapboxReplayer)

    /**
     * Debug tool that mocks location updates with an input from the [mapboxReplayer].
     */
    private val replayLocationEngine = ReplayLocationEngine(mapboxReplayer)


    private fun initNavigation() {
        MapboxNavigationApp.setup(
            NavigationOptions.Builder(requireContext())
                .accessToken(requireContext().getString(R.string.mapbox_access_token))
// comment out the location engine setting block to disable simulation
                .locationEngine(replayLocationEngine)
                .build()
        )

        val locationComponent = location.apply {
            addOnIndicatorPositionChangedListener(onPositionChangedListener)
            enabled = true
        }

        replayOriginLocation()
    }

    private fun replayOriginLocation() {
        mapboxReplayer.pushEvents(
            listOf(
                ReplayRouteMapper.mapToUpdateLocation(
                    Date().time.toDouble(),
                    Point.fromLngLat(-122.4192, 37.7627)
                )
            )
        )
        mapboxReplayer.playFirstLocation()
        mapboxReplayer.playbackSpeed(3.0)
    }

    var zoom = 12.0
    protected fun updateCamera(point: Point, bearing: Double? = 0.0) {
        if (captureing) {
            return
        }
        val mapAnimationOptionsBuilder = MapAnimationOptions.Builder()
        camera.easeTo(
            CameraOptions.Builder()
                .center(point)
                .bearing(bearing)
//                .pitch(45.0)
                .zoom(zoom)
                .build(),
            mapAnimationOptionsBuilder.build()
        )
    }

    fun updateZoom(zoom: Double, bear: Double?) {
        this.zoom = zoom
        lastPoint?.let {
            updateCamera(it, bear)
        }
    }

    protected fun updateCameraByOption(options: CameraOptions) {
        val mapAnimationOptionsBuilder = MapAnimationOptions.Builder()
        camera.easeTo(
            options,
            mapAnimationOptionsBuilder.build()
        )
    }


    open fun drawRoute(routes: List<NavigationRoute>) {
        mapboxNavigation.setNavigationRoutes(routes)
        getMapboxMap().getStyle()?.apply {
            routeLineView.hideAlternativeRoutes(this)
        }

    }

}